﻿using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.GZip;
using SharpCompress.Compressors.BZip2;
using DotNetCommon.Extensions;
using SharpCompress.Archives.Zip;
using System.Collections.Generic;
using System.Text;

namespace DotNetCommon.Compress
{
    /// <summary>
    /// 压缩和解压缩帮助类, 引用SharpZipLib
    /// </summary>
    public class CompressHelper
    {
        #region 压缩文件
        private static void save(ZipArchive archive, Stream outputStream)
        {
            archive.SaveTo(outputStream, new SharpCompress.Writers.WriterOptions(CompressionType.Deflate)
            {
                ArchiveEncoding = new ArchiveEncoding(Encoding.UTF8, Encoding.UTF8),
            });
        }
        /// <summary>
        /// 从流中压缩到流,示例:
        /// <code>
        /// CompressHelper.CompressZip(destStream, new Dictionary&lt;string, Stream>
        /// {
        ///     {"中文B222.txt",stream1 },
        ///     {"testfolder/testsubfolder-suba222.txt",stream2 },
        ///     {"testfolder-a.txt",stream3 }
        /// });
        /// </code>
        /// </summary>
        public static void CompressZip(Stream outputStream, Dictionary<string, Stream> dic)
        {
            Ensure.NotNull(outputStream, nameof(outputStream));
            Ensure.NotNullOrEmpty(dic);

            using (var archive = ZipArchive.Create())
            {
                foreach (var item in dic) archive.AddEntry(item.Key, item.Value);
                save(archive, outputStream);
            }
        }

        /// <summary>
        /// 将指定多个文件压缩到一个文件,示例:
        /// <code>
        /// CompressHelper.CompressZip(destFilePath, new Dictionary&lt;string, string>
        /// {
        /// 	{"中文B222.txt","c:/testfolder/中文B.txt" },
        /// 	{"testsubfolder/suba222.txt","c:/testfolder/testsubfolder/testsubfolder-suba.txt" },
        /// 	{"testfolder-a.txt","c:/testfolder/testfolder-a.txt" }
        /// });
        /// </code>
        /// </summary>
        public static void CompressZip(string dest, Dictionary<string, string> originFiles)
        {
            Ensure.NotNullOrEmptyOrWhiteSpace(dest);
            Ensure.NotNullOrEmpty(originFiles);

            if (!dest.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) throw new Exception("后缀名必须是.zip!");
            var dir = Path.GetDirectoryName(dest);
            if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
            if (File.Exists(dest)) File.Delete(dest);

            using (var fs = new FileStream(dest, FileMode.CreateNew))
            {
                using (var archive = ZipArchive.Create())
                {
                    foreach (var item in originFiles)
                    {
                        archive.AddEntry(item.Key, new FileInfo(item.Value));
                    }
                    save(archive, fs);
                }
            }
        }

        /// <summary>
        /// 递归压缩文件夹,示例:
        /// <code>
        /// CompressHelper.CompressZipFolder(destFilePath, new Dictionary&lt;string, string>
        /// {
        /// 	{"testfolder1","c:/test/testfolder") },
        /// 	{"testfolder2","c:/test2/testfolder") },
        /// });
        /// </code>
        /// </summary>
        public static void CompressZipFolder(string dest, Dictionary<string, string> orginFolders)
        {
            Ensure.NotNullOrEmptyOrWhiteSpace(dest);
            Ensure.NotNullOrEmpty(orginFolders);
            using (var fs = new FileStream(dest, FileMode.Create))
            {
                using (var archive = ZipArchive.Create())
                {
                    foreach (var i in orginFolders)
                    {
                        var folder = i.Value.Replace("\\", "/").TrimEnd('/') + '/';
                        var files = Directory.GetFiles(folder, "*", SearchOption.AllDirectories);
                        foreach (var f in files)
                        {
                            var file = f.Substring(folder.Length);
                            var key = $"{i.Key.Replace("\\", "/").TrimEnd('/')}/{file}";
                            archive.AddEntry(key, new FileInfo(f));
                        }
                    };
                    save(archive, fs);
                }
            }
        }

        /// <summary>
        /// 递归压缩文件夹(将文件夹及文件夹内容放到压缩包内),示例:
        /// <code>
        /// CompressHelper.CompressZipFolder(destFilePath, "c:/testfolder");
        /// </code>
        /// </summary>
        /// <remarks>注意: 得到的压缩包内会有外层文件夹的名称,如果只想压缩(<c>orginFolder</c>)的内容,参考:<seealso cref="CompressZipFolderContent(string, string)"/></remarks>
        public static void CompressZipFolder(string dest, string orginFolder)
        {
            Ensure.NotNullOrEmptyOrWhiteSpace(dest);
            Ensure.NotNullOrEmptyOrWhiteSpace(orginFolder);
            var key = orginFolder.SplitAndTrim('/', '\\').LastOrDefault();
            CompressZipFolder(dest, new Dictionary<string, string>() { { key, orginFolder } });
        }

        /// <summary>
        /// 递归压缩文件夹(将文件夹内容放到压缩包内),示例:
        /// <code>
        /// CompressHelper.CompressZipFolderContent(destFilePath, "c:/testfolder");
        /// </code>
        /// </summary>
        public static void CompressZipFolderContent(string dest, string orginFolder)
        {
            Ensure.NotNullOrEmptyOrWhiteSpace(dest);
            Ensure.NotNullOrEmptyOrWhiteSpace(orginFolder);
            CompressZipFolder(dest, new Dictionary<string, string>() { { "", orginFolder } });
        }
        #endregion

        #region 解压缩文件
        private static ReaderOptions GetReaderOptions()
        {
            return new ReaderOptions
            {
                ArchiveEncoding = new ArchiveEncoding { CustomDecoder = (bs, start, end) => LuanMaHelper.GetString(bs.Skip(start).Take(end - start).ToArray()) }
            };
        }
        /// <summary>
        /// 解压缩文件到指定文件夹, 示例:
        /// <list type="bullet">
        /// <item> CompressHelper.UnCompress("c:\\test.zip","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress("c:\\test.rar","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress("c:\\test.7z","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress("c:\\test.tar","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress("c:\\test.tar.gz","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress("c:\\test.bz2","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress("c:\\test.tar.bz2","c:\\testdir") </item>
        /// </list>
        /// </summary>
        public static void UnCompress(string src, string destDir)
        {
            Ensure.NotNullOrEmptyOrWhiteSpace(src);
            Ensure.NotNullOrEmptyOrWhiteSpace(destDir);
            if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
            if (!File.Exists(src)) throw new FileNotFoundException($"原始文件未找到,无法解压({src})!");
            if (new[] { ".rar", ".tar" }.Any(i => src.EndsWith(i, StringComparison.OrdinalIgnoreCase)))
            {
                using (Stream stream = File.OpenRead(src))
                {
                    unAr(stream, destDir);
                }
            }
            else if (src.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
            {
                if (src.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
                {
                    using (var archive = GZipArchive.Open(src, GetReaderOptions()))
                    using (var stream = archive.Entries.First().OpenEntryStream())
                    {
                        unAr(stream, destDir);
                    }
                }
                else
                {
                    //单文件
                    using (var archive = GZipArchive.Open(src, GetReaderOptions()))
                    using (var stream = archive.Entries.First().OpenEntryStream())
                    {
                        var tmpPath = Path.Combine(destDir, Path.GetFileNameWithoutExtension(src));
                        using (var fsDest = new FileStream(tmpPath, FileMode.CreateNew))
                        {
                            stream.CopyTo(fsDest);
                        }
                    }
                }
            }
            else if (src.EndsWith(".bz2", StringComparison.OrdinalIgnoreCase))
            {
                var tmpPath = Path.Combine(destDir, Path.GetFileNameWithoutExtension(src));
                if (src.EndsWith(".tar.bz2", StringComparison.OrdinalIgnoreCase))
                    tmpPath = Path.Combine(Path.GetDirectoryName(src), DateTime.Now.ToFileNameGuidString(".tar"));
                using (var fs = new FileStream(src, FileMode.Open))
                {
                    var str = new BZip2Stream(fs, SharpCompress.Compressors.CompressionMode.Decompress, true);
                    using (var destFs = new FileStream(tmpPath, FileMode.CreateNew)) str.CopyTo(destFs);
                }
                if (src.EndsWith(".tar.bz2", StringComparison.OrdinalIgnoreCase))
                {
                    using (var stream = new FileStream(tmpPath, FileMode.Open))
                    {
                        unAr(stream, destDir);
                    }
                    File.Delete(tmpPath);
                }
            }
            else if (new[] { ".zip", ".7z" }.Any(i => src.EndsWith(i, StringComparison.OrdinalIgnoreCase)))
            {
                using var archive = ArchiveFactory.Open(src, GetReaderOptions());
                foreach (var entry in archive.Entries)
                {
                    if (!entry.IsDirectory)
                    {
                        entry.WriteToDirectory(destDir, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true });
                    }
                }
            }
            else throw new Exception($"不支持的文件格式: {Path.GetExtension(src)}!");
        }

        /// <summary>
        /// 解压缩流到指定文件夹, 示例:
        /// <list type="bullet">
        /// <item> CompressHelper.UnCompress(stream,"test.zip","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress(stream,"test.rar","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress(stream,"test.7z","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress(stream,"test.tar","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress(stream","test.tar.gz","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress(stream,"test.bz2","c:\\testdir") </item>
        /// <item> CompressHelper.UnCompress(stream,"test.tar.bz2","c:\\testdir") </item>
        /// </list>
        /// </summary>
        /// <remarks>注意: 需要传入原文件名( <c>srcFileName</c> ),这样才能选择正确的解压缩算法</remarks>
        public static void UnCompress(Stream stream, string srcFileName, string destDir)
        {
            Ensure.NotNull(stream, nameof(stream));
            Ensure.NotNullOrEmptyOrWhiteSpace(destDir);
            Ensure.NotNullOrEmptyOrWhiteSpace(srcFileName);
            if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
            if (new[] { ".rar", ".tar" }.Any(i => srcFileName.EndsWith(i, StringComparison.OrdinalIgnoreCase)))
            {
                unAr(stream, destDir);
            }
            else if (srcFileName.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
            {
                var archive = GZipArchive.Open(stream);
                //单文件
                var entry = archive.Entries.First();

                if (srcFileName.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
                {
                    using (stream = entry.OpenEntryStream())
                    {
                        unAr(stream, destDir);
                    }
                }
                else entry.WriteToFile(Path.Combine(destDir, Path.GetFileNameWithoutExtension(srcFileName)));
            }
            else if (srcFileName.EndsWith(".bz2", StringComparison.OrdinalIgnoreCase))
            {
                var tmpPath = Path.Combine(destDir, Path.GetFileNameWithoutExtension(srcFileName));
                if (srcFileName.EndsWith(".tar.bz2", StringComparison.OrdinalIgnoreCase))
                    tmpPath = Path.Combine(Path.GetDirectoryName(srcFileName), DateTime.Now.ToFileNameGuidString(".tar"));
                var str = new BZip2Stream(stream, SharpCompress.Compressors.CompressionMode.Decompress, true);
                using (var destFs = new FileStream(tmpPath, FileMode.CreateNew)) str.CopyTo(destFs);
                if (srcFileName.EndsWith(".tar.bz2", StringComparison.OrdinalIgnoreCase))
                {
                    using (stream = new FileStream(tmpPath, FileMode.Open))
                    {
                        unAr(stream, destDir);
                    }
                    File.Delete(tmpPath);
                }
            }
            else if (new[] { ".zip", ".7z" }.Any(i => srcFileName.EndsWith(i, StringComparison.OrdinalIgnoreCase)))
            {
                var archive = ArchiveFactory.Open(stream, new ReaderOptions
                {
                    ArchiveEncoding = new ArchiveEncoding { CustomDecoder = (bs, start, end) => LuanMaHelper.GetString(bs.Skip(start).Take(end - start).ToArray()) }
                });
                foreach (var entry in archive.Entries)
                {
                    if (!entry.IsDirectory)
                    {
                        entry.WriteToDirectory(destDir, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true });
                    }
                }
            }
            else throw new Exception($"不支持的文件格式: {Path.GetExtension(srcFileName)}!");
        }

        private static void unAr(Stream stream, string destDir)
        {
            if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
            var reader = ReaderFactory.Open(stream, new ReaderOptions
            {
                ArchiveEncoding = new ArchiveEncoding
                {
                    CustomDecoder = (bs, start, end) => LuanMaHelper.GetString(bs.Skip(start).Take(end - start).ToArray())
                }
            });
            while (reader.MoveToNextEntry())
            {
                if (!reader.Entry.IsDirectory)
                    reader.WriteEntryToDirectory(destDir, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true });
            }
        }
        #endregion
    }
}
